package com.googlecode.ducktype;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Set;

/**
 * Allows "duck typing" or dynamic invocation based on method signature rather
 * than type hierarchy. In other words, rather than checking whether something
 * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck.
 * 
 * <P>
 * To use first use the {@link duckType} static method to indicate the object
 * you want to do Duck Typing for, then specify an interface to the {@link to} method
 * which you want to coerce the type to, e.g:
 * 
 * <pre>
 *   public interface Foo {
 *      void aMethod();
 *   }
 *   class Bar {
 *      ...
 *      public void aMethod() { ... }
 *      ...
 *   }
 *   Bar bar = ...;
 *   Foo foo = DuckType.duckType(bar).to(Foo.class);
 *   foo.aMethod();
 * </pre>
 * 
 * <p>
 * Method signatures are considered matching if all the parameter types are potentially type
 * assignable. That is every parameter type in the interface we are coercing to
 * is either a wider or narrower type of the corresponding parameter in the original interface.
 * In the case that the type is wider (ie. a superclass) then type safety is guaranteed.  In the
 * case that it is narrower then it may work or it may throw a {@link ClassCastException}.
 * 
 * 
 */
public class DuckTyper {
    
    private final Object objectToCoerce;
    
    private ProxyFactory factory = CGLibProxyFactory.getInstance();

    private DuckTyper(Object objectToCoerce) {
        this.objectToCoerce = objectToCoerce;
    }
    
    /**
     * Specify the duck typed object to coerce.
     *
     * @param object the object to coerce
     * @return
     */
    public static DuckTyper coerce(Object object) {
        return new DuckTyper(object);
    }

    /**
     * Coerce the Duck Typed object to the given interface.
     *
     * @param <T>
     * @param iface
     * @return an instance of the passed in interface that wraps the duck typed class
     */
    public <T> T to(Class<T> iface) {        
        if (isA(iface)) {
             return iface.cast(objectToCoerce);
        }
        if (quacksLikeA(iface)) {
            return factory.proxyFor(objectToCoerce, iface);
        }
        throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() +
                " to " + iface);
    }

    /**
     * Determine whether the duck typed object can be used with
     * the given interface.
     *
     * @param <T> Type of the interface to check.
     * @param iface Interface class to check
     * @return true if the object will support all the methods in the
     * interface, false otherwise.
     */
    public <T> boolean quacksLikeA(Class<T> iface) {
        if (isA(iface)) return true;
        for (Method method : iface.getMethods()) {
            if (findMethodBySignature(objectToCoerce.getClass(), method) == null) {
                return false;
            }
        }
        return true;
    }

    private <T> boolean isA(Class<T> iface) {
        return iface.isInstance(objectToCoerce);
    }
    
    private static interface MethodSelector {
        boolean matches(Method method, Method candidate);
    }
    
    private static MethodSelector SELECT_CANDIDATES_WITH_SAME_OR_WIDER_PARAMETERS_AND_SAME_OR_NARROWER_RETURN =
        new MethodSelector() {
            public boolean matches(Method method, Method candidate) {
                return candidateIsSameOrWider(method.getParameterTypes(), candidate.getParameterTypes()) && 
                    (candidateIsSameOrNarrower(method.getReturnType(), candidate.getReturnType()) ||
                            method.getReturnType() == void.class);
            }
        };
    
    private static MethodSelector SELECT_CANDIDATES_WITH_PARAMETERS_AND_RETURN_IN_SAME_HIERARCHY =
        new MethodSelector() {
            public boolean matches(Method method, Method candidate) {
                return targetIsInTypeHierarchy(method.getParameterTypes(), candidate.getParameterTypes()) && 
                    (targetIsInTypeHierarchy(method.getReturnType(), candidate.getReturnType()) ||
                            method.getReturnType() == void.class);
            }
        };
    
    
    

    // We first try to match a target against methods were the candidate accepts the same or
    // wider paramers, and returns the same or a narrower return type.
    // These are the only methods that are guaranteed to be compatible
    // since you will be passing a narrow type to a method that accepts a wider type.
    //
    // Next we try methods where the only guarantee is that the parameters and return types
    // are in the same type hierarchy, ie. we may be narrowing or widening.  In this case we
    // are trying to get the type system out of the way of the user but still guarantee that
    // a cast will be possible.  Hopefully the caller knows what they are doing
    private static final MethodSelector[] selectors = { 
      SELECT_CANDIDATES_WITH_SAME_OR_WIDER_PARAMETERS_AND_SAME_OR_NARROWER_RETURN,
      SELECT_CANDIDATES_WITH_PARAMETERS_AND_RETURN_IN_SAME_HIERARCHY      
    };

    static Method findMethodBySignature(Class<? extends Object> typeToCoerce, final Method method) {        
        Method candidate;       
        if (Proxy.isProxyClass(typeToCoerce)) {
            return findMethodForProxyInterfacesBySignature(typeToCoerce, method);
        }

        candidate = findMethodForClassMatchingExactly(typeToCoerce, method);

        if (candidate != null) return candidate;
        
        for (MethodSelector selector : selectors) {
            candidate = findUniqueMethodForClassWithSelector(typeToCoerce, method, selector);
            if (candidate != null) return candidate;
        }
        return null;
    }

    private static Method findMethodForProxyInterfacesBySignature(final Class<? extends Object> typeToCoerce, final Method method) {
        Method candidate;
        for (Class<?> iface : typeToCoerce.getInterfaces()) {
           candidate = findMethodBySignature(iface, method);
           if (candidate != null) return candidate;
        }
        return null;
    }

    private static Method findUniqueMethodForClassWithSelector(final Class<?> typeToCoerce,
            final Method method, final MethodSelector selector) {
        final Class<?>[] parameterTypes = method.getParameterTypes();
        MethodMatcher matcher = new MethodMatcher() {{
            exactMatch(method.getName());
            nrParameters(parameterTypes.length);
        }};
        Set<Method> candidates = new HashSet<Method>();
        for (Method candidate : matcher.findMethods(typeToCoerce)) {
            if (selector.matches(method, candidate)) {
                candidates.add(candidate);
            }
        }
        if (candidates.size() == 1) return candidates.iterator().next();
        return null;
    }
    
        
    private static boolean candidateIsSameOrWider(final Class<?>[] parameterTypes,
            final Class<?>[] candidateParameterTypes) {
        for (int i = 0; i < parameterTypes.length; i++) {
            if (!candidateIsSameOrWider(parameterTypes[i], candidateParameterTypes[i])) {
               return false;
            }
        }
        return true;
    }
    
    private static boolean candidateIsSameOrWider(final Class<?> parameterType,
            final Class<?> candidateParameterType) {
        return candidateParameterType.isAssignableFrom(parameterType) ||
            isAssignableWhenAutoboxed(parameterType, candidateParameterType);    
    }
    
    private static boolean isAssignableWhenAutoboxed(Class<?> parameterType,
            Class<?> candidateParameterType) {
        Class<?> autoboxedParameterType = asAutoboxed(parameterType);
        Class<?> autoboxedCandidateParameterType = asAutoboxed(candidateParameterType);
        return autoboxedParameterType.isAssignableFrom(autoboxedCandidateParameterType);
    }

    private static Class<?> asAutoboxed(Class<?> type) {
       // Returns autoboxed type as specfied by JLS 
       if (!type.isPrimitive()) return type;
       if (type == boolean.class) {
           return Boolean.class;
       }
       if (type == byte.class) {
           return Byte.class;
       }
       if (type == char.class) {
           return Character.class;
       }
       if (type == short.class) {
           return Short.class;
       }
       if (type == int.class) {
           return Integer.class;
       }
       if (type == long.class) {
           return Long.class;
       }
       if (type == float.class) {
           return Float.class;
       }
       if (type == double.class) {
           return Double.class;
       }
       return type;
    }

    private static boolean candidateIsSameOrNarrower(final Class<?> parameterType,
            final Class<?> candidateParameterType) {
        return parameterType.isAssignableFrom(candidateParameterType) || 
            isAssignableWhenAutoboxed(parameterType, candidateParameterType);    
    }
    
    private static boolean targetIsInTypeHierarchy(final Class<?>[] parameterTypes,
            final Class<?>[] candidateParameterTypes) {
        for (int i = 0; i < parameterTypes.length; i++) {
            if (!targetIsInTypeHierarchy(parameterTypes[i], candidateParameterTypes[i])) {
               return false;
            }
        }
        return true;
    }
    
    private static boolean targetIsInTypeHierarchy(final Class<?> parameterType,
            final Class<?> candidateParameterType) {
        return candidateIsSameOrNarrower(parameterType, candidateParameterType) ||
            candidateIsSameOrWider(parameterType, candidateParameterType);
    }

    private static Method findMethodForClassMatchingExactly(
            final Class<? extends Object> type, final Method method) {
        try {            
            Method candidate = type.getMethod(method.getName(), method.getParameterTypes());
            if (method.getReturnType() == candidate.getReturnType()) {
                return candidate;
            }
        } catch (NoSuchMethodException e) {
            // Squash
        }        
        return null;
    }

    public void setFactory(ProxyFactory factory) {
        this.factory = factory;
    }

    public static Object invokeProxyMethod(final Object objectToCoerce,
            Method method, Object[] args)
            throws IllegalAccessException, Throwable {
        try {
            assert findMethodBySignature(objectToCoerce.getClass(), method) != null;
            return findMethodBySignature(objectToCoerce.getClass(), method).invoke(objectToCoerce, args);
        } catch (InvocationTargetException e){
            // We need to throw the target exception so that it will match
            //   the declared exceptions on the service interface.
            //   Otherwise GWT will only send a dumb StatusCodeException
            //   without any useful info.
            throw e.getCause();
        }
    }


}
